package ch.ethz.fcl.metrofont;

import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.font.TextAttribute;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.Hashtable;

import ch.ethz.fcl.metrofont.TextureAtlas.Region;

public class TextureFont {
	private ArrayList<TextureGlyph> glyphs = null;
	private Font font = null; // font from java library
	private FontMarkup markup = null;

	private FontMetrics metrics;
	private int linespace = -1;

	public TextureFont(FontMarkup markup) {
		this.markup = markup;
		this.font = new Font(markup.getFontName(), Font.PLAIN,
				this.markup.getFontSize());

		Hashtable<TextAttribute, Object> map = new Hashtable<TextAttribute, Object>();

		if (this.markup.getKerning())
			map.put(TextAttribute.KERNING, TextAttribute.KERNING_ON);
		if (this.markup.getBold())
			map.put(TextAttribute.WEIGHT, TextAttribute.WEIGHT_BOLD);
		if (this.markup.getStrike())
			map.put(TextAttribute.STRIKETHROUGH, TextAttribute.STRIKETHROUGH_ON);
		if (this.markup.getUnderline())
			map.put(TextAttribute.UNDERLINE, TextAttribute.UNDERLINE_ON);
		if (this.markup.getItalic())
			map.put(TextAttribute.POSTURE, TextAttribute.POSTURE_OBLIQUE);
		if (this.markup.getSuperscript())
			map.put(TextAttribute.SUPERSCRIPT, TextAttribute.SUPERSCRIPT_SUPER);
		else if (this.markup.getSubscript())
			map.put(TextAttribute.SUPERSCRIPT, TextAttribute.SUPERSCRIPT_SUB);

		font = font.deriveFont(map);

		glyphs = new ArrayList<TextureGlyph>();
	}

	public void delete() {
		glyphs = null;
	}

	public boolean match(FontMarkup markup) {
		return this.markup.match(markup);
	}

	public void cacheGlyphs(TextureAtlas atlas, String string) {
		for (int i = 0; i < string.length(); i++) {
			char ch = string.charAt(i);

			if (getGlyph(ch) == null) {
				BufferedImage image = buildImage(String.valueOf(ch));
				int[] min_max = getMinMax(image);

				// Separate glyph with at least one pixel
				int height = min_max[3] - min_max[1] + 5;
				int width = min_max[2] - min_max[0] + 5;

				BufferedImage subimage = image.getSubimage(min_max[0],
						min_max[1], width, height);
				Region region = atlas.getRegion(width, height);

				int descender = min_max[3] - this.markup.getFontSize() * 2;

				int offset_x = min_max[0] - this.markup.getFontSize() + 1;
				if (ch == 32) // if the char is space
					offset_x = 0;
				int advance_x = metrics.charWidth(ch);
				TextureGlyph glyph = new TextureGlyph(ch, width - 3,
						height - 3, offset_x, descender);

				glyph.setTexCoord(region.x, region.y);
				glyph.setAddvanceX(advance_x);
				glyph.buildImage(subimage);
				atlas.setRegion(region.x, region.y, region.width,
						region.height, subimage);

				glyphs.add(glyph);

				if (i > 0 && this.markup.getKerning()) {
					generateKerning(string, i);
				}

				atlas.setChanged();
			}
		}
	}

	// to generate the kerning between two consecutive characters
	private void generateKerning(String string, int i) {
		String consecution = String.valueOf(string.charAt(i - 1))
				+ string.charAt(i);
		BufferedImage image2 = buildImage(consecution);
		int[] min_max2 = getMinMax(image2);
		int consecutiveWidth = min_max2[2] - min_max2[0] + 1;
		int kerning = consecutiveWidth
				+ getGlyph(string.charAt(i - 1)).getOffsetX()
				- getGlyph(string.charAt(i - 1)).getAddvanceX()
				- getGlyph(string.charAt(i)).getWidth()
				- getGlyph(string.charAt(i)).getOffsetX();
		int offset_x2 = min_max2[0] - this.markup.getFontSize() + 1;
		if (offset_x2 < getGlyph(string.charAt(i - 1)).getOffsetX())
			kerning = 1;
		if (string.charAt(i - 1) == 32) // if previous char is space
			kerning = 1;
		KerningPair pair = new KerningPair(string.charAt(i - 1), kerning);
		getGlyph(string.charAt(i)).addKerning(pair);
	}

	private int[] getMinMax(BufferedImage image) {
		int charTextureSize = this.markup.getFontSize() * 3;
		int[] samples = new int[charTextureSize * charTextureSize];
		image.getRGB(0, 0, charTextureSize, charTextureSize, samples, 0,
				charTextureSize);

		int minX = 1000, maxX = 0;
		int minY = 1000, maxY = 0;
		boolean pixelFound = false;

		for (int y = 0; y < charTextureSize; y++) {
			for (int x = 0; x < charTextureSize; x++) {
				int alpha = (samples[y * charTextureSize + x] >> 24) & 0xff;
				int colorR = (samples[y * charTextureSize + x] >> 16) & 0xff;
				int colorG = (samples[y * charTextureSize + x] >> 8) & 0xff;
				int colorB = samples[y * charTextureSize + x] & 0xff;
				if (alpha != 0 || colorR != 0 || colorG != 0 || colorB != 0) {
					minX = Math.min(minX, x);
					minY = Math.min(minY, y);
					maxX = Math.max(maxX, x);
					maxY = Math.max(maxY, y);
					pixelFound = true;
				}
			}
		}

		if (!pixelFound) {
			minX = minY = 0;
			maxX = maxY = 0;
		}
		int[] min_max = { minX, minY, maxX, maxY };
		return min_max;
	}

	private BufferedImage buildImage(String string) {
		int charTextureSize = this.markup.getFontSize() * 3;
		BufferedImage image = new BufferedImage(charTextureSize,
				charTextureSize, BufferedImage.TYPE_INT_ARGB);

		Graphics2D graphics = (Graphics2D) image.getGraphics();
		graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
				RenderingHints.VALUE_ANTIALIAS_ON);

		graphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
				RenderingHints.VALUE_TEXT_ANTIALIAS_ON);

		graphics.setFont(font);
		graphics.setColor(this.markup.getFontColor());

		if (metrics == null) {
			metrics = graphics.getFontMetrics();
			linespace = metrics.getHeight();
		}

		graphics.drawString(string, this.markup.getFontSize(),
				this.markup.getFontSize() * 2);

		graphics.dispose();
		return image;
	}

	public void display(TextureAtlas atlas, String msg, Pen pen, boolean display) {
		TextureGlyph glyph = null;

		Pen tempPen = new Pen(pen.getX(), pen.getY(), pen.getZ(),
				pen.getXYAngle(), pen.getYZAngle(), pen.getFontSize());

		for (int i = 0; i < msg.length(); i++) {
			glyph = getGlyph(msg.charAt(i));
			if (glyph != null) {
				if (i > 0)
					glyph.render(this, atlas, msg.charAt(i - 1), pen, display);
				else
					glyph.render(this, atlas, (char) 0, pen, display);
			}
			if (msg.charAt(i) == '\n') {
				float scale = pen.getFontSize() / markup.getFontSize();
				pen.setX(tempPen.getX() + this.getLineSpace()
						* (float) Math.sin(pen.getXYAngle()) * scale);
				pen.setY(tempPen.getY() - this.getLineSpace() * scale
						* (float) Math.cos(pen.getXYAngle()));
				tempPen.setY(tempPen.getY() - this.getLineSpace() * scale
						* (float) Math.cos(pen.getXYAngle()));
			}
		}
	}

	public int getGlyphCount() {
		return glyphs.size();
	}

	public TextureGlyph getGlyph(char ch) {
		for (int i = 0; i < glyphs.size(); i++)
			if (glyphs.get(i).getCode() == ch)
				return glyphs.get(i);
		return null;
	}

	public int getLineSpace() {
		return this.linespace;
	}

	public ArrayList<TextureGlyph> getGlyphs() {
		return this.glyphs;
	}

	public FontMarkup getMarkup() {
		return this.markup;
	}

	public void setMarkup(FontMarkup markup) {
		this.markup = markup;
	}
}
